home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / xulrunner / python / item.py < prev    next >
Encoding:
Python Source  |  2007-11-12  |  67.7 KB  |  1,915 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. from copy import copy
  19. from datetime import datetime, timedelta
  20. from gtcache import gettext as _
  21. from math import ceil
  22. from xhtmltools import unescape,xhtmlify
  23. from xml.sax.saxutils import unescape
  24. from util import checkU, returnsUnicode, checkF, returnsFilename, quoteUnicodeURL, stringify
  25. from platformutils import FilenameType
  26. import locale
  27. import os
  28. import os.path
  29. import urllib
  30. import shutil
  31. import traceback
  32.  
  33. from download_utils import cleanFilename, nextFreeFilename
  34. from feedparser import FeedParserDict
  35.  
  36. from database import DDBObject, defaultDatabase, ObjectNotFoundError
  37. from database import DatabaseConstraintError
  38. from databasehelper import makeSimpleGetSet
  39. from iconcache import IconCache
  40. from templatehelper import escape,quoteattr
  41. import types
  42. import app
  43. import template
  44. import downloader
  45. import config
  46. import dialogs
  47. import eventloop
  48. import feed
  49. import filters
  50. import menu
  51. import prefs
  52. import resources
  53. import views
  54. import random
  55. import indexes
  56. import util
  57. import adscraper
  58. import autodler
  59. import moviedata
  60. import logging
  61. import platformutils
  62. import filetypes
  63. import searchengines
  64. import fileutil
  65. import imageresize
  66.  
  67. _charset = locale.getpreferredencoding()
  68.  
  69. class Item(DDBObject):
  70.     """An item corresponds to a single entry in a feed. It has a single url
  71.     associated with it.
  72.     """
  73.  
  74.     SMALL_ICON_SIZE = (108, 81)
  75.     BIG_ICON_SIZE = (226, 170)
  76.     ICON_CACHE_SIZES = [SMALL_ICON_SIZE, BIG_ICON_SIZE]
  77.  
  78.     def __init__(self, entry, linkNumber = 0, feed_id=None, parent_id=None):
  79.         self.feed_id = feed_id
  80.         self.parent_id = parent_id
  81.         self.isContainerItem = None
  82.         self.isVideo = False
  83.         self.seen = False
  84.         self.autoDownloaded = False
  85.         self.pendingManualDL = False
  86.         self.downloadedTime = None
  87.         self.watchedTime = None
  88.         self.pendingReason = u""
  89.         self.entry = entry
  90.         self.expired = False
  91.         self.keep = False
  92.         self.videoFilename = FilenameType("")
  93.         self.eligibleForAutoDownload = True
  94.         self.duration = None
  95.         self.screenshot = None
  96.         self.resized_screenshots = {}
  97.         self.resumeTime = 0
  98.  
  99.         self.iconCache = IconCache(self)
  100.         
  101.         # linkNumber is a hack to make sure that scraped items at the
  102.         # top of a page show up before scraped items at the bottom of
  103.         # a page. 0 is the topmost, 1 is the next, and so on
  104.         self.linkNumber = linkNumber
  105.         self.creationTime = datetime.now()
  106.         self.updateReleaseDate()
  107.         self._initRestore()
  108.         self._lookForFinishedDownloader()
  109.         DDBObject.__init__(self)
  110.         self.splitItem()
  111.  
  112.     ##
  113.     # Called by pickle during serialization
  114.     def onRestore(self):
  115.         if (self.iconCache == None):
  116.             self.iconCache = IconCache (self)
  117.         else:
  118.             self.iconCache.dbItem = self
  119.             self.iconCache.requestUpdate()
  120.         # For unknown reason(s), some users still have databases with item 
  121.         # objects missing the isContainerItem attribute even after
  122.         # a db upgrade (#8819).
  123.         if not hasattr(self, 'isContainerItem'):
  124.             self.isContainerItem = None
  125.         self._initRestore()
  126.  
  127.     def _initRestore(self):
  128.         """Common code shared between onRestore and __init__."""
  129.         self.selected = False
  130.         self.active = False
  131.         self.childrenSeen = None
  132.         self.downloader = None
  133.         self.expiring = None
  134.         self.showMoreInfo = False
  135.         self.updating_movie_info = False
  136.  
  137.     def _lookForFinishedDownloader(self):
  138.         dler = downloader.lookupDownloader(self.getURL())
  139.         if dler and dler.isFinished():
  140.             self.downloader = dler
  141.             dler.addItem(self)
  142.  
  143.     getSelected, setSelected = makeSimpleGetSet(u'selected',
  144.             changeNeedsSave=False)
  145.     getActive, setActive = makeSimpleGetSet(u'active', changeNeedsSave=False)
  146.  
  147.     @returnsUnicode
  148.     def getSelectedState(self, view):
  149.         currentView = app.controller.selection.itemListSelection.currentView
  150.         if not self.selected or view != currentView:
  151.             return u'normal'
  152.         elif not self.active:
  153.             return u'selected-inactive'
  154.         else:
  155.             return u'selected'
  156.  
  157.     def toggleShowMoreInfo(self):
  158.         self.showMoreInfo = not self.showMoreInfo
  159.         self.signalChange(needsSave=False, needsUpdateXML=True)
  160.  
  161.     @returnsUnicode
  162.     def getMoreInfoState(self):
  163.         if self.showMoreInfo:
  164.             return u'more-info'
  165.         return u''
  166.  
  167.     def findChildVideos(self):
  168.         """If this item points to a directory, return the set all video files
  169.         under that directory.
  170.         """
  171.  
  172.         videos = set()
  173.         filename_root = self.getFilename()
  174.         if os.path.isdir(filename_root):
  175.             for (dirpath, dirnames, filenames) in os.walk(filename_root):
  176.                 for name in filenames:
  177.                     filename = os.path.join (dirpath, name)
  178.                     if filetypes.isVideoFilename(filename) or filetypes.isAudioFilename(filename):
  179.                         videos.add(filename)
  180.         return videos
  181.  
  182.     def findNewChildren(self):
  183.         """If this feed is a container item, walk through its directory and
  184.         find any new children.  Returns True if it found childern and ran
  185.         signalChange().
  186.         """
  187.  
  188.         filename_root = self.getFilename()
  189.         if not self.isContainerItem:
  190.             return False
  191.         if self.getState() == 'downloading':
  192.             # don't try to find videos that we're in the middle of
  193.             # re-downloading
  194.             return False
  195.         videos = self.findChildVideos()
  196.         for child in self.getChildren():
  197.             videos.discard(child.getFilename())
  198.         for video in videos:
  199.             assert video.startswith(filename_root)
  200.             offsetPath = video[len(filename_root):]
  201.             if offsetPath[0] == '/':
  202.                 offsetPath = offsetPath[1:]
  203.             FileItem (video, parent_id=self.id, offsetPath=offsetPath)
  204.         if videos:
  205.             self.signalChange()
  206.             return True
  207.         return False
  208.  
  209.     def splitItem(self):
  210.         """returns True if it ran signalChange()"""
  211.         if self.isContainerItem is not None:
  212.             return self.findNewChildren()
  213.         if not isinstance (self, FileItem) and (self.downloader is None or not self.downloader.isFinished()):
  214.             return False
  215.         filename_root = self.getFilename()
  216.         if os.path.isdir(filename_root):
  217.             videos = self.findChildVideos()
  218.             if len(videos) > 1:
  219.                 self.isContainerItem = True
  220.                 for video in videos:
  221.                     assert video.startswith(filename_root)
  222.                     offsetPath = video[len(filename_root):]
  223.                     if offsetPath[0] == '/':
  224.                         offsetPath = offsetPath[1:]
  225.                     FileItem (video, parent_id=self.id, offsetPath=offsetPath)
  226.             elif len(videos) == 1:
  227.                 self.isContainerItem = False
  228.                 for video in videos:
  229.                     assert video.startswith(filename_root)
  230.                     self.videoFilename = video[len(filename_root):]
  231.                     if self.videoFilename[0] in ('/', '\\'):
  232.                         self.videoFilename = self.videoFilename[1:]
  233.                     self.isVideo = True
  234.             else:
  235.                 if not self.getFeedURL().startswith ("dtv:directoryfeed"):
  236.                     target_dir = config.get(prefs.NON_VIDEO_DIRECTORY)
  237.                     if not filename_root.startswith(target_dir):
  238.                         if isinstance(self, FileItem):
  239.                             self.migrate (target_dir)
  240.                         else:
  241.                             self.downloader.migrate (target_dir)
  242.                 self.isContainerItem = False
  243.         else:
  244.             self.isContainerItem = False
  245.             self.videoFilename = FilenameType("")
  246.             self.isVideo = True
  247.         self.signalChange()
  248.         return True
  249.  
  250.     def removeFromPlaylists(self):
  251.         itemIDIndex = indexes.playlistsByItemID
  252.         view = views.playlists.filterWithIndex(itemIDIndex, self.getID())
  253.         for playlist in view:
  254.             playlist.removeItem(self)
  255.         view = views.playlistFolders.filterWithIndex(itemIDIndex, self.getID())
  256.         for playlist in view:
  257.             playlist.removeItem(self)
  258.  
  259.     def updateReleaseDate(self):
  260.         # This should be called whenever we get a new entry
  261.         try:
  262.             self.releaseDateObj = datetime(*self.getFirstVideoEnclosure().updated_parsed[0:7])
  263.         except:
  264.             try:
  265.                 self.releaseDateObj = datetime(*self.entry.updated_parsed[0:7])
  266.             except:
  267.                 self.releaseDateObj = datetime.min
  268.  
  269.     def checkConstraints(self):
  270.         if self.feed_id is not None:
  271.             try:
  272.                 obj = self.dd.getObjectByID(self.feed_id)
  273.             except ObjectNotFoundError:
  274.                 raise DatabaseConstraintError("my feed (%s) is not in database" % self.feed_id)
  275.             else:
  276.                 if not isinstance(obj, feed.Feed):
  277.                     msg = "feed_id points to a %s instance" % obj.__class__
  278.                     raise DatabaseConstraintError(msg)
  279.         if self.parent_id is not None:
  280.             try:
  281.                 obj = self.dd.getObjectByID(self.parent_id)
  282.             except ObjectNotFoundError:
  283.                 raise DatabaseConstraintError("my parent (%s) is not in database" % self.parent_id)
  284.             else:
  285.                 if not isinstance(obj, Item):
  286.                     msg = "parent_id points to a %s instance" % obj.__class__
  287.                     raise DatabaseConstraintError(msg)
  288.                 # If isContainerItem is None, we may be in the middle of building the children list.
  289.                 if obj.isContainerItem is not None and not obj.isContainerItem:
  290.                     msg = "parent_id is not a containerItem"
  291.                     raise DatabaseConstraintError(msg)
  292.         if self.parent_id is None and self.feed_id is None:
  293.             raise DatabaseConstraintError ("feed_id and parent_id both None")
  294.         if self.parent_id is not None and self.feed_id is not None:
  295.             raise DatabaseConstraintError ("feed_id and parent_id both not None")
  296.  
  297.     def signalChange(self, needsSave=True, needsUpdateXML=True):
  298.         self.expiring = None
  299.         try:
  300.             del self._state
  301.         except:
  302.             pass
  303.         try:
  304.             del self._size
  305.         except:
  306.             pass
  307.         if needsUpdateXML:
  308.             try:
  309.                 del self._itemXML
  310.             except:
  311.                 pass
  312.         DDBObject.signalChange(self, needsSave=needsSave)
  313.  
  314.     # Returns the rendered download-item template, hopefully from the cache
  315.     #
  316.     # viewName is the name of the view we're in. 
  317.     # view is the actual view object that we're in.
  318.     #
  319.     # Almost all of the search string is cached, but there are several pieces
  320.     # of data that must be generated on the fly:
  321.     #  * The name of the view, used for things like action:playNamedView
  322.     #  * The dragdesttype attribute -- it's based on the current selection
  323.     #  * The selected css class -- it's depends on whether the view that this
  324.     #     item is in is the view that's selected.  This matters when an item
  325.     #     is shown multiple times on a page, in different views.
  326.     #  * The channel name -- it's not displayed in the channel template.
  327.     def getItemXML(self, viewName):
  328.         try:
  329.             xml = self._itemXML
  330.         except AttributeError:
  331.             self._calcItemXML()
  332.             xml = self._itemXML
  333.         return xml.replace(self._XMLViewName, viewName)
  334.  
  335.     # Regenerates an expired item XML from the download-item template
  336.     # _XMLViewName is a random string we use for the name of the view
  337.     # _itemXML is the rendered XML
  338.     def _calcItemXML(self):
  339.         self._XMLViewName = "view%dview" % random.randint(9999999,99999999)
  340.         self._itemXML = template.fillStaticTemplate('download-item-inner', onlyBody=True, this=self, viewName = self._XMLViewName,templateState='unknown')
  341.         checkU(self._itemXML)
  342.  
  343.     #
  344.     # Returns True iff this item has never been viewed in the interface
  345.     # Note the difference between "viewed" and seen
  346.     def getViewed(self):
  347.         try:
  348.             # optimizing by trying the cached feed
  349.             return self._feed.lastViewed >= self.creationTime
  350.         except:
  351.             return self.creationTime <= self.getFeed().lastViewed 
  352.  
  353.     ##
  354.     # Returns the first video enclosure in the item
  355.     def getFirstVideoEnclosure(self):
  356.         try:
  357.             return self._firstVidEnc
  358.         except:
  359.             self._calcFirstEnc()
  360.             return self._firstVidEnc
  361.  
  362.     def _calcFirstEnc(self):
  363.         self._firstVidEnc = getFirstVideoEnclosure(self.entry)
  364.         
  365.  
  366.     ##
  367.     # Returns mime-type of the first video enclosure in the item
  368.     @returnsUnicode
  369.     def getFirstVideoEnclosureType(self):
  370.         enclosure = self.getFirstVideoEnclosure()
  371.         if enclosure and enclosure.has_key('type'):
  372.             return enclosure['type']
  373.         return None
  374.  
  375.  
  376.     ##
  377.     # Returns the URL associated with the first enclosure in the item
  378.     @returnsUnicode
  379.     def getURL(self):
  380.         self.confirmDBThread()
  381.         videoEnclosure = self.getFirstVideoEnclosure()
  382.         if videoEnclosure is not None and 'url' in videoEnclosure:
  383.             return quoteUnicodeURL(videoEnclosure['url'].replace('+', '%20'))
  384.         else:
  385.             return u''
  386.  
  387.     ##
  388.     # returns the title of the item quoted for inclusion in URLs
  389.     @returnsUnicode
  390.     def getQuotedURL(self):
  391.         return urllib.quote_plus(urllib.unquote(self.getURL().encode('ascii'))).decode('ascii')
  392.  
  393.     def hasSharableURL(self):
  394.         """Does this item have a URL that the user can share with others?
  395.  
  396.         This returns True when the item has a non-file URL.
  397.         """
  398.         url = self.getURL()
  399.         return url != u'' and not url.startswith(u"file:")
  400.  
  401.     ##
  402.     # Returns the feed this item came from
  403.     def getFeed(self):
  404.         try:
  405.             # optimizing by caching the feed
  406.             return self._feed
  407.         except:
  408.             if self.feed_id is not None:
  409.                 self._feed = self.dd.getObjectByID(self.feed_id)
  410.             elif self.parent_id is not None:
  411.                 self._feed = self.getParent().getFeed()
  412.             else:
  413.                 self._feed = None
  414.             return self._feed
  415.  
  416.     def getParent(self):
  417.         try:
  418.             return self._parent
  419.         except:
  420.             if self.parent_id is not None:
  421.                 self._parent = self.dd.getObjectByID(self.parent_id)
  422.             else:
  423.                 self._parent = self
  424.             return self._parent
  425.  
  426.     @returnsUnicode
  427.     def getFeedURL(self):
  428.         return self.getFeed().getURL()
  429.  
  430.     def feedExists(self):
  431.         return self.feed_id and self.dd.idExists(self.feed_id)
  432.  
  433.     def getChildren(self):
  434.         if self.isContainerItem:
  435.             return views.items.filterWithIndex(indexes.itemsByParent, self.id)
  436.         else:
  437.             raise ValueError("%s is not a container item" % self)
  438.  
  439.     ##
  440.     # Moves this item to another feed.
  441.     def setFeed(self, feed_id):
  442.         self.feed_id = feed_id
  443.         del self._feed
  444.         if self.isContainerItem:
  445.             for item in self.getChildren():
  446.                 del item._feed
  447.                 item.signalChange()
  448.         self.signalChange()
  449.  
  450.     def executeExpire(self):
  451.         self.confirmDBThread()
  452.         self.removeFromPlaylists()
  453.         UandA = self.getUandA()
  454.         if not self.isExternal():
  455.             self.deleteFiles()
  456.         self.expired = True
  457.         if self.isContainerItem:
  458.             for item in self.getChildren():
  459.                 item.remove()
  460.         self.isContainerItem = None
  461.         self.isVideo = False
  462.         self.videoFilename = FilenameType("")
  463.         self.seen = self.keep = self.pendingManualDL = False
  464.         self.watchedTime = None
  465.         self.duration = None
  466.         if self.screenshot:
  467.             try:
  468.                 os.remove(self.screenshot)
  469.             except:
  470.                 pass
  471.         # This should be done even if screenshot = ""
  472.         self.screenshot = None
  473.         if self.isExternal():
  474.             if self.isDownloaded():
  475.                 new_item = FileItem (self.getVideoFilename(), feed_id=self.feed_id, parent_id=self.parent_id, deleted=True)
  476.                 if self.downloader is not None:
  477.                     self.downloader.setDeleteFiles(False)
  478.             self.remove()
  479.         else:
  480.             self.signalChange()
  481.  
  482.     ##
  483.     # Marks this item as expired
  484.     def expire(self):
  485.         title = _("Removing %s") % os.path.basename(self.getTitle())
  486.         if self.isExternal():
  487.             if self.isContainerItem:
  488.                 description = _("""\
  489. Would you like to delete this folder and all of its videos or just remove \
  490. its entry from the Library?""")
  491.                 button = dialogs.BUTTON_DELETE_FILES
  492.             else:
  493.                 if self.isDownloaded():
  494.                     description = _("""\
  495. Would you like to delete this file or just remove its entry from the \
  496. Library?""")
  497.                     button = dialogs.BUTTON_DELETE_FILE
  498.                 else:
  499.                     self.executeExpire()
  500.                     return
  501.             d = dialogs.ThreeChoiceDialog(title, description,
  502.                     dialogs.BUTTON_REMOVE_ENTRY, button,
  503.                     dialogs.BUTTON_CANCEL)
  504.             def callback(dialog):
  505.                 if not self.idExists():
  506.                     return
  507.                 if dialog.choice == button:
  508.                     self.deleteFiles()
  509.                 if dialog.choice in (button, dialogs.BUTTON_REMOVE_ENTRY):
  510.                     self.executeExpire()
  511.     
  512.             d.run(callback)
  513.         elif self.isContainerItem:
  514.             description = _("""\
  515. This item is a folder.  When you remove a folder, any items inside that \
  516. folder will be deleted.""")
  517.             d = dialogs.ChoiceDialog(title, description,
  518.                                      dialogs.BUTTON_DELETE_FILES,
  519.                                      dialogs.BUTTON_CANCEL)
  520.             def callback(dialog):
  521.                 if self.idExists() and dialog.choice == dialogs.BUTTON_DELETE_FILES:
  522.                     self.executeExpire()
  523.             d.run(callback)
  524.         else:
  525.             self.executeExpire()
  526.  
  527.     def stopUpload (self):
  528.         if self.downloader:
  529.             self.downloader.stopUpload()
  530.  
  531.     def startUpload (self):
  532.         if self.downloader:
  533.             self.downloader.startUpload()
  534.  
  535.     @returnsUnicode
  536.     def getString(self, when):
  537.         """Get the expiration time a string to display to the user."""
  538.         offset = when - datetime.now()
  539.         if offset.days > 0:
  540.             result = _("%d days") % offset.days
  541.         elif offset.seconds > 3600:
  542.             result = _("%d hours") % (ceil(offset.seconds/3600.0))
  543.         else:
  544.             result = _("%d minutes") % (ceil(offset.seconds/60.0))
  545.         return result
  546.  
  547.     @returnsUnicode
  548.     def getExpirationString(self):
  549.         """Get the expiration time a string to display to the user."""
  550.         expireTime = self.getExpirationTime()
  551.         if expireTime is None:
  552.             return u""
  553.         else:
  554.             return _('Expires in %s') % self.getString (expireTime)
  555.  
  556.     @returnsUnicode
  557.     def getPausedString(self):
  558.         """Get the expiration time a string to display to the user."""
  559.         retryTime = None
  560.         if self.downloader:
  561.             if self.downloader.getState() == u'offline':
  562.                 retryTime = self.downloader.status['retryTime']
  563.                 if retryTime is None:
  564.                     return ""
  565.                 else:
  566.                     return _('Will retry in %s') % self.getString (retryTime)
  567.             else:
  568.                 return _('Paused')
  569.         else:
  570.             return u""
  571.  
  572.     @returnsUnicode
  573.     def getDragType(self):
  574.         if self.isDownloaded():
  575.             return u'downloadeditem'
  576.         else:
  577.             return u'item'
  578.  
  579.     @returnsUnicode
  580.     def getEmblemCSSClass(self):
  581.         if self.getState() == u'newly-downloaded':
  582.             return u'newly-downloaded'
  583.         elif self.getState() == u'new':
  584.             return u'new'
  585.         else:
  586.             return u''
  587.  
  588.     @returnsUnicode
  589.     def getEmblemCSSString(self):
  590.         if self.getState() == u'newly-downloaded':
  591.             return u'UNWATCHED'
  592.         elif self.getState() == u'new':
  593.             return u'NEW'
  594.         else:
  595.             return u''
  596.  
  597.     def getUandA(self):
  598.         """Get whether this item is new, or newly-downloaded, or neither."""
  599.         state = self.getState()
  600.         if state == u'new':
  601.             return (0, 1)
  602.         elif state == u'newly-downloaded':
  603.             return (1, 0)
  604.         else:
  605.             return (0, 0)
  606.  
  607.     def getExpirationTime(self):
  608.         """Get the time when this item will expire. 
  609.         Returns a datetime object,  or None if it doesn't expire.
  610.         """
  611.  
  612.         self.confirmDBThread()
  613.         if self.getWatchedTime() is None or not self.isDownloaded():
  614.             return None
  615.         ufeed = self.getFeed()
  616.         if ufeed.expire == u'never' or (ufeed.expire == u'system'
  617.                 and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0):
  618.             return None
  619.         else:
  620.             if ufeed.expire == u"feed":
  621.                 expireTime = ufeed.expireTime
  622.             elif ufeed.expire == u"system":
  623.                 expireTime = timedelta(days=config.get(prefs.EXPIRE_AFTER_X_DAYS))
  624.             return self.getWatchedTime() + expireTime
  625.  
  626.     def getWatchedTime(self):
  627.         if not self.getSeen():
  628.             return None
  629.         if self.isContainerItem and self.watchedTime == None:
  630.             self.watchedTime = datetime.min
  631.             for item in self.getChildren():
  632.                 childTime = item.getWatchedTime()
  633.                 if childTime is None:
  634.                     self.watchedTime = None
  635.                     return None
  636.                 if childTime > self.watchedTime:
  637.                     self.watchedTime = childTime
  638.             self.signalChange()
  639.         return self.watchedTime
  640.  
  641.     def getExpiring(self):
  642.         if self.expiring is None:
  643.             if not self.getSeen():
  644.                 self.expiring = False
  645.             else:
  646.                 ufeed = self.getFeed()
  647.                 if (self.keep or ufeed.expire == u'never' or 
  648.                         (ufeed.expire == u'system' and
  649.                             config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0)):
  650.                     self.expiring = False
  651.                 else:
  652.                     self.expiring = True
  653.         return self.expiring
  654.  
  655.     ##
  656.     # returns true iff video has been seen
  657.     # Note the difference between "viewed" and "seen"
  658.     def getSeen(self):
  659.         self.confirmDBThread()
  660.         if self.isContainerItem:
  661.             if self.childrenSeen is None:
  662.                 self.childrenSeen = True
  663.                 for item in self.getChildren():
  664.                     if not item.seen:
  665.                         self.childrenSeen = False
  666.                         break
  667.             return self.childrenSeen
  668.         else:
  669.             return self.seen
  670.  
  671.     ##
  672.     # Marks the item as seen
  673.     def markItemSeen(self):
  674.         self.confirmDBThread()
  675.         if self.seen == False:
  676.             self.seen = True
  677.             if self.watchedTime is None:
  678.                 self.watchedTime = datetime.now()
  679.             self.clearParentsChildrenSeen()
  680.             self.signalChange()
  681.  
  682.     def clearParentsChildrenSeen(self):
  683.         if self.parent_id:
  684.             parent = self.getParent()
  685.             parent.childrenSeen = None
  686.             parent.signalChange()
  687.  
  688.     def markItemUnseen(self):
  689.         self.confirmDBThread()
  690.         if self.isContainerItem:
  691.             self.childrenSeen = False
  692.             for item in self.getChildren():
  693.                 item.seen = False
  694.                 item.signalChange()
  695.             self.signalChange()
  696.         else:
  697.             if self.seen == False:
  698.                 return
  699.             self.seen = False
  700.             self.watchedTime = None
  701.             self.clearParentsChildrenSeen()
  702.             self.signalChange()
  703.  
  704.     @returnsUnicode
  705.     def getRSSID(self):
  706.         self.confirmDBThread()
  707.         return self.entry["id"]
  708.  
  709.     def removeRSSID(self):
  710.         self.confirmDBThread()
  711.         if 'id' in self.entry:
  712.             del self.entry['id']
  713.             self.signalChange()
  714.  
  715.     def setAutoDownloaded(self,autodl = True):
  716.         self.confirmDBThread()
  717.         if autodl != self.autoDownloaded:
  718.             self.autoDownloaded = autodl
  719.             self.signalChange()
  720.  
  721.     @eventloop.asIdle
  722.     def setResumeTime(self, position):
  723.         if not self.idExists():
  724.             return
  725.         position = int(position)
  726.         if self.resumeTime != position:
  727.             self.resumeTime = position
  728.             self.signalChange()
  729.  
  730.     @returnsUnicode
  731.     def getPendingReason(self):
  732.         self.confirmDBThread()
  733.         return self.pendingReason
  734.  
  735.     ##
  736.     # Returns true iff item was auto downloaded
  737.     def getAutoDownloaded(self):
  738.         self.confirmDBThread()
  739.         return self.autoDownloaded
  740.  
  741.     ##
  742.     # Returns the linkNumber
  743.     def getLinkNumber(self):
  744.         self.confirmDBThread()
  745.         return self.linkNumber
  746.  
  747.     ##
  748.     # Starts downloading the item
  749.     def download(self,autodl=False):
  750.         autodler.resumeDownloader()
  751.         self.confirmDBThread()
  752.         manualDownloadCount = views.manualDownloads.len()
  753.         self.expired = self.keep = self.seen = False
  754.  
  755.         if ((not autodl) and 
  756.                 manualDownloadCount >= config.get(prefs.MAX_MANUAL_DOWNLOADS)):
  757.             self.pendingManualDL = True
  758.             self.pendingReason = u"queued for download" # FIXME:
  759.                                                         # Should this
  760.                                                         # be
  761.                                                         # translated --NN
  762.             self.signalChange()
  763.             return
  764.         else:
  765.             self.setAutoDownloaded(autodl)
  766.             self.pendingManualDL = False
  767.  
  768.         if self.downloader is None:
  769.             self.downloader = downloader.getDownloader(self)
  770.         if self.downloader is not None:
  771.             self.downloader.setChannelName (platformutils.unicodeToFilename(self.getChannelTitle(True)))
  772.             if self.downloader.isFinished():
  773.                 self.onDownloadFinished()
  774.             else:
  775.                 self.downloader.start()
  776.         self.signalChange()
  777.  
  778.     def pause(self):
  779.         if self.downloader:
  780.             self.downloader.pause()
  781.  
  782.     def resume(self):
  783.         self.download(self.getAutoDownloaded())
  784.  
  785.     def isPendingManualDownload(self):
  786.         self.confirmDBThread()
  787.         return self.pendingManualDL
  788.  
  789.     def isEligibleForAutoDownload(self):
  790.         self.confirmDBThread()
  791.         if self.getState() not in (u'new', u'not-downloaded'):
  792.             return False
  793.         if self.downloader and self.downloader.getState() in (u'failed',
  794.                 u'stopped', u'paused'):
  795.             return False
  796.         ufeed = self.getFeed()
  797.         if ufeed.getEverything:
  798.             return True
  799.         return self.eligibleForAutoDownload
  800.  
  801.     def isPendingAutoDownload(self):
  802.         return (self.getFeed().isAutoDownloadable() and
  803.                 self.isEligibleForAutoDownload())
  804.  
  805.     def isFailedDownload(self):
  806.         return self.downloader and self.downloader.getState() == u'failed'
  807.  
  808.     ##
  809.     # Returns a link to the thumbnail of the video
  810.     @returnsUnicode
  811.     def getThumbnailURL(self):
  812.         self.confirmDBThread()
  813.         # Try to get the thumbnail specific to the video enclosure
  814.         videoEnclosure = self.getFirstVideoEnclosure()
  815.         if videoEnclosure is not None:
  816.             try:
  817.                 return videoEnclosure["thumbnail"]["url"].decode("ascii","replace")
  818.             except:
  819.                 pass 
  820.         # Try to get any enclosure thumbnail
  821.         for enclosure in self.entry.enclosures:
  822.             try:
  823.                 return enclosure["thumbnail"]["url"].decode('ascii','replace')
  824.             except KeyError:
  825.                 pass
  826.         # Try to get the thumbnail for our entry
  827.         try:
  828.             return self.entry["thumbnail"]["url"].decode('ascii','replace')
  829.         except:
  830.             return None
  831.  
  832.     @returnsUnicode
  833.     def getThumbnail (self):
  834.         self.confirmDBThread()
  835.         if self.showMoreInfo:
  836.             width, height = Item.BIG_ICON_SIZE
  837.         else:
  838.             width, height = Item.SMALL_ICON_SIZE
  839.         if self.iconCache.isValid():
  840.             path = self.iconCache.getResizedFilename(width, height)
  841.             return resources.absoluteUrl(path)
  842.         elif self.screenshot:
  843.             path = self.getResizedScreenshot(width, height)
  844.             return resources.absoluteUrl(path)
  845.         elif self.isContainerItem:
  846.             return resources.url(u"images/container-icon.png")
  847.         else:
  848.             feedThumbnail = self.getFeed().getItemThumbnail(width, height)
  849.             if feedThumbnail is not None:
  850.                 return feedThumbnail
  851.             elif self.showMoreInfo:
  852.                 return resources.url(u"images/thumb-more-info.png")
  853.             else: 
  854.                 return resources.url(u"images/thumb.png")
  855.  
  856.     ##
  857.     # returns the title of the item
  858.     @returnsUnicode
  859.     def getTitle(self):
  860.         try:
  861.             return self.entry.title
  862.         except:
  863.             try:
  864.                 enclosure = self.getFirstVideoEnclosure()
  865.                 return enclosure["url"].decode('ascii','replace')
  866.             except:
  867.                 return u""
  868.  
  869.     ##
  870.     # returns the title of the item quoted for inclusion in URLs
  871.     @returnsUnicode
  872.     def getQuotedTitle(self):
  873.         return urllib.quote_plus(self.getTitle().encode('utf8')).decode('ascii', 'replace')
  874.  
  875.     @returnsUnicode
  876.     def getChannelTitle(self, allowSearchFeedTitle=False):
  877.         implClass = self.getFeed().actualFeed.__class__
  878.         if implClass in (feed.RSSFeedImpl, feed.ScraperFeedImpl):
  879.             return self.getFeed().getTitle()
  880.         elif implClass == feed.SearchFeedImpl and allowSearchFeedTitle:
  881.             return searchengines.getLastEngineTitle()
  882.         else:
  883.             return u''
  884.  
  885.     ##
  886.     # Returns the raw description of the video (unicode)
  887.     @returnsUnicode
  888.     def getRawDescription(self):
  889.         self.confirmDBThread()
  890.         try:
  891.             enclosure = self.getFirstVideoEnclosure()
  892.             return enclosure["text"]
  893.         except:
  894.             try:
  895.                 return self.entry.description
  896.             except:
  897.                 return u''
  898.  
  899.     ##
  900.     # Returns valid XHTML containing a description of the video (str)
  901.     @returnsUnicode
  902.     def getDescription(self):
  903.         rawDescription = self.getRawDescription()
  904.         try:
  905.             purifiedDescription = adscraper.purify(rawDescription)
  906.             return xhtmlify (u'<span>%s</span>' % (unescape(purifiedDescription),), filterFontTags=True)
  907.         except:
  908.             try:
  909.                 return xhtmlify (u'<span>%s</span>' % (unescape(rawDescription),))
  910.             except:
  911.                 return u'<span />'
  912.  
  913.     ##
  914.     # Returns valid XHTML containing the ad (str)
  915.     def getAd(self):
  916.         rawDescription = self.getRawDescription()
  917.         try:
  918.             rawAd = adscraper.scrape(rawDescription)
  919.             return xhtmlify (u'<span>%s</span>' % (unescape(rawAd),))
  920.         except:
  921.             return u'<span />'
  922.  
  923.     def looksLikeTorrent(self):
  924.         """Returns true if we think this item is a torrent.  (For items that
  925.         haven't been downloaded this uses the file extension which isn't
  926.         totally reliable).
  927.         """
  928.  
  929.         if self.downloader is not None:
  930.             return self.downloader.getType() == u'bittorrent'
  931.         else:
  932.             return self.getURL().endswith(u'.torrent')
  933.  
  934.     ##
  935.     # Returns formatted XHTML with release date, duration, format, and size
  936.     @returnsUnicode
  937.     def getDetails(self):
  938.         details = []
  939.         reldate = self.getReleaseDate()
  940.         format = self.getFormat()
  941.         size = self.getSizeForDisplay()
  942.         link = self.getLink()
  943.  
  944.         if self.isContainerItem:
  945.             children = self.getChildren()
  946.             details.append(u'<span class="details-count">%s items</span>' % len(children))
  947.         if len(reldate) > 0:
  948.             details.append(u'<span class="details-date">%s</span>' % escape(reldate))
  949.         if len(size) > 0:
  950.             details.append(u'<span class="details-size">%s</span>' % escape(size))
  951.         if len(format) > 0:
  952.             details.append(u'<span class="details-format">%s</span>' % escape(format))
  953.         if self.looksLikeTorrent():
  954.             details.append(u'<span class="details-torrent">%s</span>' % _("TORRENT"))
  955.         if len(link) > 0 and link != self.getURL():
  956.             details.append(u'<a class="details-link" href="%s">%s</span>' % (quoteattr(link), _("WEB PAGE")))
  957.         out = u'<BR>'.join(details)
  958.         return out
  959.  
  960.     def isTransferring(self):
  961.         return self.downloader and self.downloader.getState() in (u'uploading', u'downloading')
  962.  
  963.     def getDownloadDetails(self):
  964.         status = self.downloader.status
  965.         details = [
  966.             (_('Total Down:'), formatSizeForDetails(status.get('currentSize', 0))),
  967.         ]
  968.         if status.get("reasonFailed"):
  969.             details.append((_('Error:'), status['reasonFailed']))
  970.         return details
  971.  
  972.     def getTorrentDetails(self):
  973.         status = self.downloader.status
  974.         return [
  975. #            (_('Seeders:'), status.get('seeders', 0)),
  976. #            (_('Leechers:'), status.get('leechers', 0)),
  977.             (_('Down Rate:'), formatRateForDetails(status.get('rate', 0))),
  978.             (_('Down Total:'), formatSizeForDetails(
  979.                 status.get('currentSize', 0))),
  980.             (_('Up Rate:'), formatRateForDetails(status.get('upRate', 0))),
  981.             (_('Up Total:'), formatSizeForDetails(status.get('uploaded', 0) * 1024 * 1024)),
  982.         ]
  983.  
  984.     def getItemDetails(self):
  985.         rv = []
  986.         
  987.         link = self.getLink()
  988.         if link:
  989.             rv.append((_('Web page:'), util.makeAnchor(_('permalink'), link)))
  990.  
  991.         url = self.getURL()
  992.         if url and not url.startswith("file:"):
  993.             rv.append((_('File link:'), util.makeAnchor(_('direct link to file'),
  994.                                               url)))
  995.         rv.append((_('File type:'), self.getFormat()))
  996.  
  997.         if self.isDownloaded():
  998.             basename = os.path.basename(self.getFilename())
  999.             basename = util.clampText(basename, 40)
  1000.             linkEventURL = u'revealItem?item=%d' % self.getID()
  1001.             if self.isContainerItem:
  1002.                 label = _("REVEAL LOCAL FOLDER")
  1003.             else:
  1004.                 label = _("REVEAL LOCAL FILE")
  1005.             link = util.makeEventURL(label, linkEventURL)
  1006.             rv.append((_('Filename:'), u"%s<BR />%s" % (platformutils.filenameToUnicode(basename), link)))
  1007.         return rv
  1008.  
  1009.  
  1010.     def getTorrentDetailsFinished(self):
  1011.         status = self.downloader.status
  1012.         return [
  1013.             (_('Down Total'), formatSizeForDetails(
  1014.                 status.get('currentSize', 0))),
  1015.             (_('Up Total'), formatSizeForDetails(status.get('uploaded', 0) * 1024 * 1024)),
  1016.         ]
  1017.  
  1018.     def makeMoreInfoTable(self, title, moreInfoData):
  1019.         lines = []
  1020.         lines.append(u'<h3>%s</h3>' % title)
  1021.         lines.append(u'<table cellpadding="0" cellspacing="0">')
  1022.         for label, text in moreInfoData:
  1023.             lines.append(u'<tr><td class="label">%s</td>'
  1024.                     u'<td class="value">%s</td></tr>' % (label, text))
  1025.         lines.append(u'</table>')
  1026.         return u'\n'.join(lines)
  1027.  
  1028.     ## 
  1029.     # Returns formatted XHTML with download info
  1030.     @returnsUnicode
  1031.     def getMoreInfo(self):
  1032.         details = [
  1033.             self.makeMoreInfoTable(_('Item Details'), self.getItemDetails()),
  1034.         ]
  1035.         # helper function to keep things from getting too verbose below
  1036.         def addTable(label, data):
  1037.             details.append(self.makeMoreInfoTable(label, data))
  1038.         if self.looksLikeTorrent():
  1039.             if self.isTransferring():
  1040.                 addTable(_('Torrent Details'), self.getTorrentDetails())
  1041.             elif self.downloader and self.downloader.isFinished():
  1042.                 addTable(_('Torrent Details <i>stopped</i>'),
  1043.                         self.getTorrentDetailsFinished())
  1044.         elif ((self.getState() == u'downloading' and not self.pendingManualDL)
  1045.                 or self.isFailedDownload()):
  1046.             addTable(_('Download Details'), self.getDownloadDetails())
  1047.         return u'\n'.join(details)
  1048.  
  1049.  
  1050.     ##
  1051.     # Stops downloading the item
  1052.     def deleteFiles(self):
  1053.         self.confirmDBThread()
  1054.         if self.downloader is not None:
  1055.             self.downloader.removeItem(self)
  1056.             self.downloader = None
  1057.             self.signalChange()
  1058.  
  1059.     def getState(self):
  1060.         """Get the state of this item.  The state will be on of the following:
  1061.  
  1062.         * new -- User has never seen this item
  1063.         * not-downloaded -- User has seen the item, but not downloaded it
  1064.         * downloading -- Item is currently downloading
  1065.         * newly-downloaded -- Item has been downoladed, but not played
  1066.         * expiring -- Item has been played and is set to expire
  1067.         * saved -- Item has been played and has been saved
  1068.         * expired -- Item has expired.
  1069.  
  1070.         Uses caching to prevent recalculating state over and over
  1071.         """
  1072.         try:
  1073.             return self._state
  1074.         except AttributeError:
  1075.             self._calcState()
  1076.             return self._state
  1077.  
  1078.     # Recalculate the state of an item after a change
  1079.     @returnsUnicode
  1080.     def _calcState(self):
  1081.         self.confirmDBThread()
  1082.         # FIXME, 'failed', and 'paused' should get download icons.  The user
  1083.         # should be able to restart or cancel them (put them into the stopped
  1084.         # state).
  1085.         if (self.downloader is None  or 
  1086.                 self.downloader.getState() in (u'failed', u'stopped')):
  1087.             if self.pendingManualDL:
  1088.                 self._state = u'downloading'
  1089.             elif self.expired:
  1090.                 self._state = u'expired'
  1091.             elif (self.getViewed() or
  1092.                     (self.downloader and
  1093.                         self.downloader.getState() in (u'failed', u'stopped'))):
  1094.                 self._state = u'not-downloaded'
  1095.             else:
  1096.                 self._state = u'new'
  1097.         elif self.downloader.getState() in (u'offline', u'paused'):
  1098.             if self.pendingManualDL:
  1099.                 self._state = u'downloading'
  1100.             else:
  1101.                 self._state = u'paused'
  1102.         elif not self.downloader.isFinished():
  1103.             self._state = u'downloading'
  1104.         elif not self.getSeen():
  1105.             self._state = u'newly-downloaded'
  1106.         elif self.getExpiring():
  1107.             self._state = u'expiring'
  1108.         else:
  1109.             self._state = u'saved'
  1110.  
  1111.     @returnsUnicode    
  1112.     def getChannelCategory(self):
  1113.         """Get the category to use for the channel template.  
  1114.         
  1115.         This method is similar to getState(), but has some subtle differences.
  1116.         getState() is used by the download-item template and is usually more
  1117.         useful to determine what's actually happening with an item.
  1118.         getChannelCategory() is used by by the channel template to figure out
  1119.         which heading to put an item under.
  1120.  
  1121.         * downloading and not-downloaded are grouped together as
  1122.           not-downloaded
  1123.         * Newly downloaded and downloading items are always new if
  1124.           their feed hasn't been marked as viewed after the item's pub
  1125.           date.  This is so that when a user gets a list of items and
  1126.           starts downloading them, the list doesn't reorder itself.
  1127.           Once they start watching them, then it reorders itself.
  1128.         """
  1129.  
  1130.         self.confirmDBThread()
  1131.         if self.downloader is None or not self.downloader.isFinished():
  1132.             if not self.getViewed():
  1133.                 return u'new'
  1134.             if self.expired:
  1135.                 return u'expired'
  1136.             else:
  1137.                 return u'not-downloaded'
  1138.         elif not self.getSeen():
  1139.             if not self.getViewed():
  1140.                 return u'new'
  1141.             return u'newly-downloaded'
  1142.         elif self.getExpiring():
  1143.             return u'expiring'
  1144.         else:
  1145.             return u'saved'
  1146.  
  1147.     def isDownloadable(self):
  1148.         return self.getState() in (u'new', u'not-downloaded', u'expired')
  1149.  
  1150.     def isDownloaded(self):
  1151.         return self.getState() in (u"newly-downloaded", u"expiring", u"saved")
  1152.  
  1153.     def showSaveButton(self):
  1154.         return self.getState() in (u'newly-downloaded', u'expiring') and not self.keep
  1155.  
  1156.     def showSaved(self):
  1157.         return self.getState() in (u'saved',) or (self.getState() in (u'newly-downloaded', u'expiring') and self.keep)
  1158.  
  1159.     def showTrashButton(self):
  1160.         return self.isDownloaded() or (self.getFeedURL() == u'dtv:manualFeed'
  1161.                 and self.getState() not in (u'downloading', u'paused'))
  1162.  
  1163.     @returnsUnicode
  1164.     def getFailureReason(self):
  1165.         self.confirmDBThread()
  1166.         if self.downloader is not None:
  1167.             return self.downloader.getShortReasonFailed()
  1168.         else:
  1169.             return u""
  1170.     
  1171.     ##
  1172.     # Returns the size of the item to be displayed.
  1173.     def getSizeForDisplay(self):
  1174.         return util.formatSizeForUser(self.getSize())
  1175.  
  1176.     def getSize(self):
  1177.         if not hasattr(self, "_size"):
  1178.             self._size = self._getSize()
  1179.         return self._size
  1180.  
  1181.     ##
  1182.     # Returns the size of the item. We use the following methods to get the
  1183.     # size:
  1184.     #
  1185.     # Physical size of a downloaded file
  1186.     # HTTP content-length
  1187.     # RSS enclosure tag value.
  1188.     def _getSize(self):
  1189.         fname = self.getFilename()
  1190.         if self.isDownloaded():
  1191.             try:
  1192.                 return util.getsize(fname)
  1193.             except OSError:
  1194.                 return 0
  1195.         elif self.downloader is not None:
  1196.             return self.downloader.getTotalSize()
  1197.         else:
  1198.             try:
  1199.                 return int(self.getFirstVideoEnclosure()['length'])
  1200.             except:
  1201.                 return 0
  1202.  
  1203.     ##
  1204.     # returns status of the download in plain text
  1205.     @returnsUnicode
  1206.     def getCurrentSize(self):
  1207.         if self.downloader is not None:
  1208.             size = self.downloader.getCurrentSize()
  1209.         else:
  1210.             size = 0
  1211.         return util.formatSizeForUser(size)
  1212.  
  1213.     ##
  1214.     # Returns the download progress in absolute percentage [0.0 - 100.0].
  1215.     def downloadProgress(self):
  1216.         progress = 0
  1217.         self.confirmDBThread()
  1218.         if self.downloader is None:
  1219.             return 0
  1220.         else:
  1221.             size = self.downloader.getTotalSize()
  1222.             dled = self.downloader.getCurrentSize()
  1223.             if size == 0:
  1224.                 return 0
  1225.             else:
  1226.                 return (100.0*dled) / size
  1227.  
  1228.     def gotContentLength(self):
  1229.         if self.downloader is None:
  1230.             return False
  1231.         else:
  1232.             return self.downloader.getTotalSize() != -1
  1233.  
  1234.     ##
  1235.     # Returns the width of the progress bar corresponding to the current
  1236.     # download progress. This doesn't really belong here and even forces
  1237.     # to use a hardcoded constant, but the templating system doesn't 
  1238.     # really leave any other choice.
  1239.     def downloadProgressWidth(self):
  1240.         fullWidth = 112  # width of resource:channelview-progressbar-bg.png
  1241.         progress = self.downloadProgress() / 100.0
  1242.         if progress == 0:
  1243.             return 0
  1244.         return int(progress * fullWidth)
  1245.  
  1246.     ##
  1247.     # Returns string containing three digit percent finished
  1248.     # "000" through "100".
  1249.     @returnsUnicode
  1250.     def threeDigitPercentDone(self):
  1251.         return u'%03d' % int(self.downloadProgress())
  1252.  
  1253.     def downloadInProgress(self):
  1254.         return self.downloader is not None and self.downloader.getETA() != 0
  1255.  
  1256.     ##
  1257.     # Returns string with estimate time until download completes
  1258.     @returnsUnicode
  1259.     def downloadETA(self):
  1260.         if self.downloader is not None:
  1261.             totalSecs = self.downloader.getETA()
  1262.             if totalSecs == -1:
  1263.                 return _('downloading...')
  1264.         else:
  1265.             totalSecs = 0
  1266.         mins, secs = divmod(totalSecs, 60)
  1267.         hours, mins = divmod(mins, 60)
  1268.         if hours > 0:
  1269.             time = u"%d:%02d:%02d" % (hours, mins, secs)
  1270.             return _("%s remaining") % time
  1271.         else:
  1272.             time = u"%d:%02d" % (mins, secs)
  1273.             return _("%s remaining") % time
  1274.  
  1275.     @returnsUnicode
  1276.     def getStartupActivity(self):
  1277.         if self.pendingManualDL:
  1278.             return self.pendingReason
  1279.         elif self.downloader:
  1280.             return self.downloader.getStartupActivity()
  1281.         else:
  1282.             return _("starting up...")
  1283.  
  1284.     ##
  1285.     # Returns the download rate
  1286.     @returnsUnicode
  1287.     def downloadRate(self):
  1288.         rate = 0
  1289.         unit = u"KB/s"
  1290.         if self.downloader is not None:
  1291.             rate = self.downloader.getRate()
  1292.         else:
  1293.             rate = 0
  1294.         rate /= 1024
  1295.         if rate > 1024:
  1296.             rate /= 1024
  1297.             unit = u"MB/s"
  1298.         if rate > 1024:
  1299.             rate /= 1024
  1300.             unit = u"GB/s"
  1301.             
  1302.         return u"%d%s" % (rate, unit)
  1303.  
  1304.     ##
  1305.     # Returns the published date of the item
  1306.     @returnsUnicode
  1307.     def getPubDate(self):
  1308.         return getReleaseDate()
  1309.     
  1310.     ##
  1311.     # Returns the published date of the item as a datetime object
  1312.     def getPubDateParsed(self):
  1313.         return self.getReleaseDateObj()
  1314.  
  1315.     ##
  1316.     # returns the date this video was released or when it was published
  1317.     @returnsUnicode
  1318.     def getReleaseDate(self):
  1319.         try:
  1320.             return self.getReleaseDateObj().strftime("%b %d %Y").decode(_charset)
  1321.         except:
  1322.             return u""
  1323.  
  1324.     ##
  1325.     # returns the date this video was released or when it was published
  1326.     def getReleaseDateObj(self):
  1327.         return self.releaseDateObj
  1328.  
  1329.     ##
  1330.     # returns the length of the video in seconds
  1331.     def getDurationValue(self):
  1332.         secs = 0
  1333.         if self.duration not in (-1, None):
  1334.             secs = self.duration / 1000
  1335.         return secs
  1336.  
  1337.     ##
  1338.     # returns string with the play length of the video
  1339.     @returnsUnicode
  1340.     def getDuration(self, emptyIfZero=True):
  1341.         secs = self.getDurationValue()
  1342.         if secs == 0:
  1343.             if emptyIfZero:
  1344.                 return u""
  1345.             else:
  1346.                 return "n/a"
  1347.         return u"%02d:%02d" % (secs/60, secs % 60)
  1348.  
  1349.     ##
  1350.     # returns string with the format of the video
  1351.     KNOWN_MIME_TYPES = (u'audio', u'video')
  1352.     KNOWN_MIME_SUBTYPES = (u'mov', u'wmv', u'mp4', u'mp3', u'mpg', u'mpeg', u'avi', u'x-flv', u'x-msvideo', u'm4v', u'mkv', u'm2v')
  1353.     MIME_SUBSITUTIONS = {
  1354.         u'QUICKTIME': u'MOV',
  1355.     }
  1356.     @returnsUnicode
  1357.     def getFormat(self, emptyForUnknown=True):
  1358.         if self.looksLikeTorrent():
  1359.             return u'.torrent'
  1360.         try:
  1361.             enclosure = self.entry['enclosures'][0]
  1362.             try:
  1363.                 extension = enclosure['url'].split('.')[-1].lower().decode('ascii','replace')
  1364.             except:
  1365.                 extension == u''
  1366.             # Hack for mp3s, "mpeg audio" isn't clear enough
  1367.             if extension.lower() == u'mp3':
  1368.                 return u'.mp3'
  1369.             if enclosure.has_key('type') and len(enclosure['type']) > 0:
  1370.                 mtype, subtype = enclosure['type'].decode('ascii','replace').split('/')
  1371.                 mtype = mtype.lower()
  1372.                 if mtype in self.KNOWN_MIME_TYPES:
  1373.                     format = subtype.split(';')[0].upper()
  1374.                     if mtype == u'audio':
  1375.                         format += u' AUDIO'
  1376.                     if format.startswith(u'X-'):
  1377.                         format = format[2:]
  1378.                     return u'.%s' % self.MIME_SUBSITUTIONS.get(format, format).lower()
  1379.             if extension in self.KNOWN_MIME_SUBTYPES:
  1380.                 return u'.%s' % extension
  1381.         except:
  1382.             pass
  1383.         if emptyForUnknown:
  1384.             return u""
  1385.         else:
  1386.             return u"unknown"
  1387.  
  1388.     ##
  1389.     # return keyword tags associated with the video separated by commas
  1390.     @returnsUnicode
  1391.     def getTags(self):
  1392.         self.confirmDBThread()
  1393.         try:
  1394.             return self.entry.categories.join(u", ")
  1395.         except:
  1396.             return u""
  1397.  
  1398.     ##
  1399.     # return the license associated with the video
  1400.     @returnsUnicode
  1401.     def getLicence(self):
  1402.         self.confirmDBThread()
  1403.         try:
  1404.             return self.entry.license
  1405.         except:
  1406.             try:
  1407.                 return self.getFeed().getLicense()
  1408.             except:
  1409.                 return u""
  1410.  
  1411.     ##
  1412.     # return the people associated with the video, separated by commas
  1413.     @returnsUnicode
  1414.     def getPeople(self):
  1415.         ret = []
  1416.         self.confirmDBThread()
  1417.         try:
  1418.             for role in self.getFirstVideoEnclosure().roles:
  1419.                 for person in self.getFirstVideoEnclosure().roles[role]:
  1420.                     ret.append(person)
  1421.             for role in self.entry.roles:
  1422.                 for person in self.entry.roles[role]:
  1423.                     ret.append(person)
  1424.         except:
  1425.             pass
  1426.         return u', '.join(ret)
  1427.  
  1428.     ##
  1429.     # returns the URL of the webpage associated with the item
  1430.     def getLink(self):
  1431.         self.confirmDBThread()
  1432.         try:
  1433.             return self.entry.link.decode('ascii','replace')
  1434.         except:
  1435.             return u""
  1436.  
  1437.     ##
  1438.     # returns the URL of the payment page associated with the item
  1439.     def getPaymentLink(self):
  1440.         self.confirmDBThread()
  1441.         try:
  1442.             return self.getFirstVideoEnclosure().payment_url.decode('ascii','replace')
  1443.         except:
  1444.             try:
  1445.                 return self.entry.payment_url.decode('ascii','replace')
  1446.             except:
  1447.                 return u""
  1448.  
  1449.     ##
  1450.     # returns a snippet of HTML containing a link to the payment page
  1451.     # HTML has already been sanitized by feedparser
  1452.     @returnsUnicode
  1453.     def getPaymentHTML(self):
  1454.         self.confirmDBThread()
  1455.         try:
  1456.             ret = self.getFirstVideoEnclosure().payment_html
  1457.         except:
  1458.             try:
  1459.                 ret = self.entry.payment_html
  1460.             except:
  1461.                 ret = u""
  1462.         # feedparser returns escaped CDATA so we either have to change its
  1463.         # behavior when it parses dtv:paymentlink elements, or simply unescape
  1464.         # here...
  1465.         return u'<span>' + unescape(ret) + u'</span>'
  1466.  
  1467.     def makeContextMenu(self, templateName, view):
  1468.         c = app.controller # easier/shorter to type
  1469.         if self.isDownloaded():
  1470.             if templateName in ('playlist', 'playlist-folder'):
  1471.                 label = _('Remove From Playlist')
  1472.             else:
  1473.                 label = _('Remove From the Library')
  1474.             items = [
  1475.                 (lambda: c.playView(view, self.getID()), _('Play')),
  1476.                 (lambda: c.playView(view, self.getID(), True), 
  1477.                     _('Play Just This Video')),
  1478.                 (c.addToNewPlaylist, _('Add to new playlist')),
  1479.                 (c.removeCurrentItems, label),
  1480.             ]
  1481.             if self.getSeen():
  1482.                 items.append((self.markItemUnseen, _('Mark as Unwatched')))
  1483.             else:
  1484.                 items.append((self.markItemSeen, _('Mark as Watched')))
  1485.                 
  1486.             if self.downloader and self.downloader.getState() == 'finished' and self.downloader.getType() == 'bittorrent':
  1487.                 items.append((self.startUpload, _('Restart Upload')))
  1488.         elif self.getState() == 'downloading':
  1489.             items = [(self.expire, _('Cancel Download')), (self.pause, _('Pause Download'))]
  1490.         else:
  1491.             items = [(self.download, _('Download'))]
  1492.         return menu.makeMenu(items)
  1493.  
  1494.     ##
  1495.     # Updates an item with new data
  1496.     #
  1497.     # @param entry a dict object containing the new data
  1498.     def update(self, entry):
  1499.         UandA = self.getUandA()
  1500.         self.confirmDBThread()
  1501.         try:
  1502.             self.entry = entry
  1503.             self.iconCache.requestUpdate()
  1504.             self.updateReleaseDate()
  1505.             self._calcFirstEnc()
  1506.         finally:
  1507.             self.signalChange()
  1508.  
  1509.     def onDownloadFinished(self):
  1510.         """Called when the download for this item finishes."""
  1511.  
  1512.         self.confirmDBThread()
  1513.         self.downloadedTime = datetime.now()
  1514.         if not self.splitItem():
  1515.             self.signalChange()
  1516.         moviedata.movieDataUpdater.requestUpdate (self)
  1517.  
  1518.         for other in views.items:
  1519.             if other.downloader is None and other.getURL() == self.getURL():
  1520.                 other.downloader = self.downloader
  1521.                 self.downloader.addItem(other)
  1522.                 other.signalChange(needsSave=False)
  1523.         
  1524.         app.delegate.notifyDownloadCompleted(self)
  1525.  
  1526.     def getResizedScreenshot(self, width, height):
  1527.         try:
  1528.             return imageresize.getImage(self.resized_screenshots, width, height)
  1529.         except KeyError:
  1530.             return self.screenshot
  1531.  
  1532.     def resizeScreenshot(self):
  1533.         imageresize.removeResizedFiles(self.resized_screenshots)
  1534.         if self.screenshot:
  1535.             self.resized_screenshots = imageresize.multiResizeImage(
  1536.                     self.screenshot, self.ICON_CACHE_SIZES)
  1537.         else:
  1538.             self.resized_screenshots = {}
  1539.  
  1540.     def save(self):
  1541.         self.confirmDBThread()
  1542.         if self.keep != True:
  1543.             self.keep = True
  1544.             self.signalChange()
  1545.  
  1546.     ##
  1547.     # gets the time the video was downloaded
  1548.     # Only valid if the state of this item is "finished"
  1549.     def getDownloadedTime(self):
  1550.         if self.downloadedTime is None:
  1551.             return datetime.min
  1552.         else:
  1553.             return self.downloadedTime
  1554.  
  1555.     ##
  1556.     # Returns the filename of the first downloaded video or the empty string
  1557.     # NOTE: this will always return the absolute path to the file.
  1558.     @returnsFilename
  1559.     def getFilename(self):
  1560.         self.confirmDBThread()
  1561.         try:
  1562.             return self.downloader.getFilename()
  1563.         except:
  1564.             return FilenameType("")
  1565.  
  1566.     ##
  1567.     # Returns the filename of the first downloaded video or the empty string
  1568.     # NOTE: this will always return the absolute path to the file.
  1569.     @returnsFilename
  1570.     def getVideoFilename(self):
  1571.         self.confirmDBThread()
  1572.         if self.videoFilename:
  1573.             return os.path.join (self.getFilename(), self.videoFilename)
  1574.         else:
  1575.             return self.getFilename()
  1576.  
  1577.     def isNonVideoFile(self):
  1578.         # isContainerItem can be False or None.
  1579.         return self.isContainerItem != True and not self.isVideo
  1580.  
  1581.     def isExternal(self):
  1582.         """Returns True iff this item was not downloaded from a Democracy
  1583.         channel.
  1584.         """
  1585.         return self.feed_id is not None and self.getFeedURL() == 'dtv:manualFeed'
  1586.  
  1587.     def isPlayable(self):
  1588.         """Returns True iff this item should have a play button."""
  1589.         if not self.isContainerItem:
  1590.             return self.isDownloaded() and self.getVideoFilename()
  1591.         else:
  1592.             return self.isDownloaded() and len(self.getChildren()) > 0
  1593.  
  1594.     def getRSSEntry(self):
  1595.         self.confirmDBThread()
  1596.         return self.entry
  1597.  
  1598.     def migrateChildren (self, newdir):
  1599.         if self.isContainerItem:
  1600.             for item in self.getChildren():
  1601.                 item.migrate(newdir)
  1602.         
  1603.  
  1604.     def remove(self):
  1605.         if self.downloader is not None:
  1606.             self.downloader.removeItem(self)
  1607.             self.downloader = None
  1608.         if self.iconCache is not None:
  1609.             self.iconCache.remove()
  1610.             self.iconCache = None
  1611.         imageresize.removeResizedFiles(self.resized_screenshots)
  1612.         if self.isContainerItem:
  1613.             for item in self.getChildren():
  1614.                 item.remove()
  1615.         DDBObject.remove(self)
  1616.  
  1617.     def setupLinks(self):
  1618.         """This is called after we restore the database.  Since we don't store
  1619.         references between objects, we need a way to reconnect downloaders to
  1620.         the items after the restore.
  1621.         """
  1622.         
  1623.         if not isinstance (self, FileItem) and self.downloader is None:
  1624.             self.downloader = downloader.getExistingDownloader(self)
  1625.             if self.downloader is not None:
  1626.                 self.signalChange(needsSave=False)
  1627.         self.splitItem()
  1628.         # This must come after reconnecting the downloader
  1629.         if self.isContainerItem is not None and not os.path.exists(self.getFilename()):
  1630.             self.executeExpire()
  1631.             return
  1632.         if self.screenshot and not os.path.exists(self.screenshot):
  1633.             self.screenshot = None
  1634.             self.signalChange()
  1635.         if self.duration is None or self.screenshot is None:
  1636.             moviedata.movieDataUpdater.requestUpdate (self)
  1637.  
  1638.     def __str__(self):
  1639.         return "Item - %s" % self.getTitle()
  1640.  
  1641. def reconnectDownloaders():
  1642.     reconnected = set()
  1643.     for item in views.items:
  1644.         item.setupLinks()
  1645.         reconnected.add(item.downloader)
  1646.     for downloader in views.remoteDownloads:
  1647.         if downloader not in reconnected:
  1648.             logging.warn("removing orphaned downloader: %s", downloader.url)
  1649.             downloader.remove()
  1650.     manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1651.     manualItems = views.items.filterWithIndex(indexes.itemsByFeed,
  1652.             manualFeed.getID())
  1653.     for item in manualItems:
  1654.         if item.downloader is None and item.__class__ == Item:
  1655.             logging.warn("removing cancelled external torrent: %s", item)
  1656.             item.remove()
  1657.  
  1658. def getEntryForFile(filename):
  1659.     return FeedParserDict({'title':platformutils.filenameToUnicode(os.path.basename(filename)),
  1660.             'enclosures':[{'url': resources.url(filename)}]})
  1661.  
  1662. def getEntryForURL(url, contentType=None):
  1663.     if contentType is None:
  1664.         contentType = u'video/x-unknown'
  1665.     else:
  1666.         contentType = unicode(contentType)
  1667.     return FeedParserDict({'title' : url,
  1668.             'enclosures':[{'url' : url, 'type' : contentType}]})
  1669.  
  1670. ##
  1671. # An Item that exists as a local file
  1672. class FileItem(Item):
  1673.  
  1674.     def __init__(self,filename, feed_id=None, parent_id=None, offsetPath=None, deleted=False):
  1675.         checkF(filename)
  1676.         filename = os.path.abspath(filename)
  1677.         self.filename = filename
  1678.         self.deleted = deleted
  1679.         self.offsetPath = offsetPath
  1680.         self.shortFilename = cleanFilename(os.path.basename(self.filename))
  1681.         Item.__init__(self, getEntryForFile(filename), feed_id=feed_id, parent_id=parent_id)
  1682.         moviedata.movieDataUpdater.requestUpdate (self)
  1683.  
  1684.     @returnsUnicode
  1685.     def getState(self):
  1686.         if self.deleted:
  1687.             return u"expired"
  1688.         elif self.getSeen():
  1689.             return u"saved"
  1690.         else:
  1691.             return u"newly-downloaded"
  1692.  
  1693.     def getChannelCategory(self):
  1694.         """Get the category to use for the channel template.  
  1695.         
  1696.         This method is similar to getState(), but has some subtle differences.
  1697.         getState() is used by the download-item template and is usually more
  1698.         useful to determine what's actually happening with an item.
  1699.         getChannelCategory() is used by by the channel template to figure out
  1700.         which heading to put an item under.
  1701.  
  1702.         * downloading and not-downloaded are grouped together as
  1703.           not-downloaded
  1704.         * Items are always new if their feed hasn't been marked as viewed
  1705.           after the item's pub date.  This is so that when a user gets a list
  1706.           of items and starts downloading them, the list doesn't reorder
  1707.           itself.
  1708.         * Child items match their parents for expiring, where in
  1709.           getState, they always act as not expiring.
  1710.         """
  1711.  
  1712.         self.confirmDBThread()
  1713.         if self.deleted:
  1714.             return u'expired'
  1715.         elif not self.getSeen():
  1716.             return u'newly-downloaded'
  1717.         else:
  1718.             if self.parent_id and self.getParent().getExpiring():
  1719.                 return u'expiring'
  1720.             else:
  1721.                 return u'saved'
  1722.  
  1723.     def getExpiring(self):
  1724.         return False
  1725.  
  1726.     def showSaveButton(self):
  1727.         return False
  1728.  
  1729.     def getViewed(self):
  1730.         return True
  1731.  
  1732.     def isExternal(self):
  1733.         return self.parent_id is None
  1734.  
  1735.     def executeExpire(self):
  1736.         self.confirmDBThread()
  1737.         self.removeFromPlaylists()
  1738.         if self.isContainerItem:
  1739.             for item in self.getChildren():
  1740.                 item.remove()
  1741.         if not os.path.exists (self.filename):
  1742.             # item whose file has been deleted outside of DP
  1743.             self.remove()
  1744.         elif self.feed_id is None: 
  1745.             self.deleted = True
  1746.             self.signalChange()
  1747.         else:
  1748.             # external item that the user deleted in DP
  1749.             url = self.getFeedURL()
  1750.             if url.startswith ("dtv:manualFeed") or url.startswith ("dtv:singleFeed"):
  1751.                 self.remove()
  1752.             else:
  1753.                 self.deleted = True
  1754.                 self.signalChange()
  1755.  
  1756.     def deleteFiles(self):
  1757.         try:
  1758.             if self.getParent():
  1759.                 dler = self.getParent().downloader
  1760.                 if dler:
  1761.                     dler.stop(False)
  1762.             if os.path.isfile(self.filename):
  1763.                 os.remove(self.filename)
  1764.             elif os.path.isdir(self.filename):
  1765.                 shutil.rmtree(self.filename)
  1766.         except:
  1767.             logging.warn("WARNING: error deleting files:\n%s",
  1768.                     traceback.format_exc())
  1769.  
  1770.     def getDownloadedTime(self):
  1771.         self.confirmDBThread()
  1772.         try:
  1773.             return datetime.fromtimestamp(os.path.getctime(self.filename))
  1774.         except:
  1775.             return datetime.min
  1776.  
  1777.     @returnsFilename
  1778.     def getFilename(self):
  1779.         try:
  1780.             return self.filename
  1781.         except:
  1782.             return FilenameType("")
  1783.  
  1784.     def download(self,autodl=False):
  1785.         self.deleted = False
  1786.         self.signalChange()
  1787.  
  1788.     def updateReleaseDate(self):
  1789.         # This should be called whenever we get a new entry
  1790.         try:
  1791.             self.releaseDateObj = datetime.fromtimestamp(os.path.getmtime(self.filename))
  1792.         except:
  1793.             self.releaseDateObj = datetime.min
  1794.  
  1795.     def getReleaseDateObj(self):
  1796.         if self.parent_id:
  1797.             return self.getParent().releaseDateObj
  1798.         else:
  1799.             return self.releaseDateObj
  1800.  
  1801.     def migrate(self, newDir):
  1802.         self.confirmDBThread()
  1803.         if self.parent_id:
  1804.             parent = self.getParent()
  1805.             self.filename = os.path.join (parent.getFilename(), self.offsetPath)
  1806.             return
  1807.         if self.shortFilename is None:
  1808.             logging.warn("""\
  1809. can't migrate download because we don't have a shortFilename!
  1810. filename was %s""", stringify(self.filename))
  1811.             return
  1812.         newFilename = os.path.join(newDir, self.shortFilename)
  1813.         if self.filename == newFilename:
  1814.             return
  1815.         if os.path.exists(self.filename):
  1816.             newFilename = nextFreeFilename(newFilename)
  1817.             def callback():
  1818.                 self.filename = newFilename
  1819.                 self.signalChange()
  1820.             fileutil.migrate_file(self.filename, newFilename, callback)
  1821.         elif os.path.exists(newFilename):
  1822.             self.filename = newFilename
  1823.             self.signalChange()
  1824.         self.migrateChildren(newDir)
  1825.  
  1826.     def setupLinks(self):
  1827.         if self.shortFilename is None:
  1828.             if self.parent_id is None:
  1829.                 self.shortFilename = cleanFilename(os.path.basename(self.filename))
  1830.             else:
  1831.                 parent_file = self.getParent().getFilename()
  1832.                 if self.filename.startswith(parent_file):
  1833.                     self.shortFilename = cleanFilename(self.filename[len(parent_file):])
  1834.                 else:
  1835.                     logging.warn("%s is not a subdirectory of %s",
  1836.                             self.filename, parent_file)
  1837.         self.updateReleaseDate()
  1838.         Item.setupLinks(self)
  1839.  
  1840. def expireItems(items):
  1841.     if len(items) == 1:
  1842.         return items[0].expire()
  1843.  
  1844.     hasContainers = False
  1845.     hasExternalItems = False
  1846.     for item in items:
  1847.         if item.isContainerItem:
  1848.             hasContainers = True
  1849.         elif item.isExternal():
  1850.             hasExternalItems = True
  1851.         if hasContainers and hasExternalItems:
  1852.             break
  1853.  
  1854.     title = _("Removing %s items") % len(items)
  1855.     if hasExternalItems:
  1856.         description = _("""One or more of these videos was not downloaded \
  1857. from a channel.  Would you like to delete these items or just remove their \
  1858. entries from the Library?""")
  1859.     else:
  1860.         description = u"Are you sure you want to delete all %s videos?" % \
  1861.                 len(items)
  1862.  
  1863.     if hasContainers:
  1864.         description += u"\n\n" + _("""\
  1865. One or more of these items is a folder.  When you remove or delete a folder, \
  1866. any items inside that folder will also be removed or deleted.""")
  1867.  
  1868.     if hasExternalItems:
  1869.         d = dialogs.ThreeChoiceDialog(title, description,
  1870.                 dialogs.BUTTON_REMOVE_ENTRY, dialogs.BUTTON_DELETE_FILES,
  1871.                 dialogs.BUTTON_CANCEL)
  1872.     else:
  1873.         d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_OK,
  1874.                 dialogs.BUTTON_CANCEL)
  1875.  
  1876.     def callback(dialog):
  1877.         if dialog.choice == dialogs.BUTTON_DELETE_FILES:
  1878.             for item in items:
  1879.                 if item.idExists() and isinstance (item, FileItem):
  1880.                     item.deleteFiles()
  1881.         if dialog.choice in (dialogs.BUTTON_OK, dialogs.BUTTON_REMOVE_ENTRY,
  1882.                 dialogs.BUTTON_DELETE_FILES):
  1883.             for item in items:
  1884.                 if item.idExists():
  1885.                     item.executeExpire()
  1886.     d.run(callback)
  1887.  
  1888. def getFirstVideoEnclosure(entry):
  1889.     """Find the first video enclosure in a feedparser entry.  Returns the
  1890.     enclosure, or None if no video enclosure is found.
  1891.     """
  1892.  
  1893.     try:
  1894.         enclosures = entry.enclosures
  1895.     except (KeyError, AttributeError):
  1896.         return None
  1897.     for enclosure in enclosures:
  1898.         if filetypes.isVideoEnclosure(enclosure):
  1899.             return enclosure
  1900.     return None
  1901.  
  1902. @returnsUnicode
  1903. def formatRateForDetails(bytes):
  1904.     """Format a download/upload rate for the more-details view."""
  1905.     sizeFmt = util.formatSizeForUser(bytes, zeroString=u"-")
  1906.     if bytes > 0:
  1907.         return sizeFmt + u"/s"
  1908.     else:
  1909.         return sizeFmt
  1910.  
  1911. @returnsUnicode
  1912. def formatSizeForDetails(bytes):
  1913.     """Format a disk size for the more-details view."""
  1914.     return util.formatSizeForUser(bytes, zeroString=u"-")
  1915.